package com.luych.toolbox.data.service.service;

import com.luych.toolbox.common.enums.*;
import com.luych.toolbox.common.feign.intf.base.service.UserFeign;
import com.luych.toolbox.common.feign.param.UserAndPassParam;
import com.luych.toolbox.common.feign.param.data.service.DataSourceParam;
import com.luych.toolbox.common.feign.view.base.service.UserView;
import com.luych.toolbox.common.feign.view.data.service.*;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import com.luych.toolbox.data.service.constant.Constant;
import com.luych.toolbox.data.service.core.*;
import com.luych.toolbox.data.service.dao.*;
import com.luych.toolbox.data.service.entity.*;
import com.luych.toolbox.data.service.job.DataSourceHealthCheckJob;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class DataSourceService
        extends BaseService<DataSourceView, DataSourceParam, DataSource>
        implements ApplicationListener<ApplicationReadyEvent> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceService.class);

    private static final BaseConvert<DataSourceParam, DataSource> dataSourceParamToEntity = DataSource::new;
    private static final BaseConvert<DataSource, DataSourceView> dataSourceEntityToView = DataSourceView::new;

    private final Boolean rescheduleWhenStart;

    private final Scheduler scheduler;

    private final DataSourceDao dataSourceDao;
    private final DataAgentDao dataAgentDao;
    private final DataProjectRDataSourceDao dataProjectRDataSourceDao;
    private final DataBackupPlanDao dataBackupPlanDao;
    private final DataBackupHistoryDao dataBackupHistoryDao;
    private final DataRecoveryHistoryDao dataRecoveryHistoryDao;
    private final DataComparePlanDao dataComparePlanDao;
    private final DataCompareHistoryDao dataCompareHistoryDao;
    private final DataProjectDao dataProjectDao;
    private final DataUpgradeDao dataUpgradeDao;
    private final DataUpgradeDetailDao dataUpgradeDetailDao;

    private final UserFeign userFeign;

    @Autowired
    public DataSourceService(@Value("${spring.quartz.rescheduleWhenStart:false}") Boolean rescheduleWhenStart,
                             Scheduler scheduler,
                             DataSourceDao dataSourceDao,
                             DataAgentDao dataAgentDao, DataProjectRDataSourceDao dataProjectRDataSourceDao,
                             DataBackupPlanDao dataBackupPlanDao,
                             DataBackupHistoryDao dataBackupHistoryDao,
                             DataRecoveryHistoryDao dataRecoveryHistoryDao,
                             DataComparePlanDao dataComparePlanDao,
                             DataCompareHistoryDao dataCompareHistoryDao,
                             DataProjectDao dataProjectDao,
                             DataUpgradeDao dataUpgradeDao,
                             DataUpgradeDetailDao dataUpgradeDetailDao,
                             UserFeign userFeign) {
        super(dataSourceDao, dataSourceParamToEntity, dataSourceEntityToView);
        this.rescheduleWhenStart = rescheduleWhenStart;
        this.scheduler = scheduler;
        this.dataAgentDao = dataAgentDao;
        this.dataBackupPlanDao = dataBackupPlanDao;
        this.dataBackupHistoryDao = dataBackupHistoryDao;
        this.dataRecoveryHistoryDao = dataRecoveryHistoryDao;
        this.dataComparePlanDao = dataComparePlanDao;
        this.dataCompareHistoryDao = dataCompareHistoryDao;
        this.dataProjectDao = dataProjectDao;
        this.dataUpgradeDao = dataUpgradeDao;
        this.dataUpgradeDetailDao = dataUpgradeDetailDao;
        this.userFeign = userFeign;
        this.dataSourceDao = dataSourceDao;
        this.dataProjectRDataSourceDao = dataProjectRDataSourceDao;
    }

    @Override
    protected DataSourceView fill(DataSourceView view) {
        dataSourceDao.findById(view.getId()).map(DataSource::getOwnerId).map(userFeign::get).ifPresent(view::setOwner);
        return view;
    }

    private JobKey getJobKey(String id) {
        return new JobKey(id, DataSourceHealthCheckJob.class.getName());
    }

    private JobDetail getJobDetail(String id) {
        Map<String, Object> jobParam = new HashMap<String, Object>() {{
            this.put("id", id);
        }};
        return JobBuilder.newJob(DataSourceHealthCheckJob.class)
                .withIdentity(this.getJobKey(id))
                .usingJobData(new JobDataMap(jobParam))
                .build();
    }

    private Trigger getJobTrigger(String id) {
        return TriggerBuilder.newTrigger()
                .withIdentity(id, DataSourceHealthCheckJob.class.getName())
                .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(10))
                .build();
    }

    @Override
    public DataSourceView save(DataSourceParam param) {
        // check owner
        UserView owner = userFeign.get(param.getOwnerId());
        if (owner == null) {
            throw new HttpException(HttpStatus.BAD_REQUEST, Constant.DATA_SOURCE_SAVE_OWNER_ERROR);
        }

        // save datasource
        DataSource dataSourceParam = dataSourceParamToEntity.apply(param);
        dataSourceParam.setStatus(DataSourceStatus.WAITING);
        Optional<DataSource> dataSourceOpt = dataSourceDao.findById(param.getId());
        dataSourceOpt.map(DataSource::getSuperUser).ifPresent(dataSourceParam::setSuperUser);
        dataSourceOpt.map(DataSource::getSuperPass).ifPresent(dataSourceParam::setSuperPass);
        DataSource dataSourceResult = dataSourceDao.save(dataSourceParam);
        return this.fill(dataSourceEntityToView.apply(dataSourceResult));
    }

    @Override
    public DataSourceView create(DataSourceParam param) {
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getType(), "Param type can't be empty");
        Assert.notNull(param.getHost(), "Param host can't be empty");
        Assert.notNull(param.getPort(), "Param port can't be empty");
        Assert.notNull(param.getUser(), "Param user can't be empty");
        Assert.notNull(param.getPass(), "Param pass can't be empty");
        Assert.notNull(param.getOwnerId(), "Param ownerId can't be empty");

        // check name exists
        if (dataSourceDao.findByName(param.getName()).isPresent()) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.DATA_SOURCE_SAVE_NAME_USED);
        }

        // save
        DataSourceView view = this.save(param);

        // schedule
        try {
            scheduler.scheduleJob(this.getJobDetail(view.getId()), this.getJobTrigger(view.getId()));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        return view;
    }

    @Override
    public DataSourceView modify(DataSourceParam param) {
        Assert.notNull(param.getId(), "Param id can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getType(), "Param type can't be empty");
        Assert.notNull(param.getHost(), "Param host can't be empty");
        Assert.notNull(param.getPort(), "Param port can't be empty");
        Assert.notNull(param.getUser(), "Param user can't be empty");
        Assert.notNull(param.getPass(), "Param pass can't be empty");
        Assert.notNull(param.getOwnerId(), "Param ownerId can't be empty");

        // check name exists
        Optional<DataSource> dataSourceOptOfName = dataSourceDao.findByName(param.getName());
        if (dataSourceOptOfName.isPresent() && !dataSourceOptOfName.get().getId().equals(param.getId())) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.DATA_SOURCE_SAVE_NAME_USED);
        }

        // save
        DataSourceView view = this.save(param);

        // schedule
        try {
            scheduler.deleteJob(this.getJobKey(view.getId()));
            scheduler.scheduleJob(this.getJobDetail(view.getId()), this.getJobTrigger(view.getId()));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        return view;
    }

    @Override
    public void remove(String id) {
        if (dataProjectRDataSourceDao.countByDataSourceId(id) > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_SOURCE_DELETE_REJECT_USED_BY_PROJECT);
        }
        if (dataBackupPlanDao.countByDataSourceId(id) > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_SOURCE_DELETE_REJECT_USED_BY_BACKUP_PLAN);
        }
        if (dataBackupHistoryDao.countByDataSourceId(id) > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_SOURCE_DELETE_REJECT_USED_BY_BACKUP_HISTORY);
        }
        if (dataComparePlanDao.countByDataSourceOneIdOrDataSourceTwoId(id, id) > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_SOURCE_DELETE_REJECT_USED_BY_COMPARE_PLAN);
        }
        if (dataCompareHistoryDao.countByDataSourceOneIdOrDataSourceTwoId(id, id) > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_SOURCE_DELETE_REJECT_USED_BY_COMPARE_HISTORY);
        }

        // schedule
        try {
            scheduler.deleteJob(this.getJobKey(id));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        dataSourceDao.deleteById(id);
    }

    public List<Map<String, Object>> query(String id, String sql) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(sql, "Param sql can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        try {
            return connection.query(sql);
        } catch (Exception e) {
            throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage());
        }
    }

    public void execute(String id, String sql) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(sql, "Param sql can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        try {
            connection.execute(sql);
        } catch (Exception e) {
            throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage());
        }
    }

    public String getTableCreateSql(String id, String tableName, DataSourceType dataSourceType, CaseType caseType) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(tableName, "Param tableName can't be empty");

        if (caseType == null) {
            caseType = CaseType.UNCHANGE;
        }

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        CoreDatabase database = connection.getDatabase();
        CoreTable table = database.getTables().stream().filter(tbl -> tbl.getName().equals(tableName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_TABLE_NOT_FOUND));
        return dataSourceType == null ? table.getCreateSql(caseType) : CoreBuilder.buildTable(dataSourceType, table).getCreateSql(caseType);
    }

    public String getTableRemoveSql(String id, String tableName, DataSourceType dataSourceType, CaseType caseType) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(tableName, "Param tableName can't be empty");

        if (caseType == null) {
            caseType = CaseType.UNCHANGE;
        }

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        CoreDatabase database = connection.getDatabase();
        CoreTable table = database.getTables().stream().filter(tbl -> tbl.getName().equals(tableName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_TABLE_NOT_FOUND));
        return dataSourceType == null ? table.getRemoveSql(caseType) : CoreBuilder.buildTable(dataSourceType, table).getRemoveSql(caseType);
    }

    public String getColumnCreateSql(String id, String tableName, String columnName, DataSourceType dataSourceType, CaseType caseType) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(tableName, "Param tableName can't be empty");
        Assert.notNull(columnName, "Param columnName can't be empty");

        if (caseType == null) {
            caseType = CaseType.UNCHANGE;
        }

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        CoreDatabase database = connection.getDatabase();
        CoreTable table = database.getTables().stream().filter(tbl -> tbl.getName().equals(tableName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_TABLE_NOT_FOUND));
        CoreColumn column = table.getColumns().stream().filter(clmn -> clmn.getName().equals(columnName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COLUMN_NOT_FOUND));
        return dataSourceType == null ? column.getCreateSql(caseType) : CoreBuilder.buildColumn(dataSourceType, column).getCreateSql(caseType);
    }

    public String getColumnModifySql(String id, String tableName, String columnName, DataSourceType dataSourceType, CaseType caseType) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(tableName, "Param tableName can't be empty");
        Assert.notNull(columnName, "Param columnName can't be empty");

        if (caseType == null) {
            caseType = CaseType.UNCHANGE;
        }

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        CoreDatabase database = connection.getDatabase();
        CoreTable table = database.getTables().stream().filter(tbl -> tbl.getName().equals(tableName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_TABLE_NOT_FOUND));
        CoreColumn column = table.getColumns().stream().filter(clmn -> clmn.getName().equals(columnName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COLUMN_NOT_FOUND));
        return dataSourceType == null ? column.getModifySql(caseType) : CoreBuilder.buildColumn(dataSourceType, column).getModifySql(caseType);
    }

    public String getColumnRemoveSql(String id, String tableName, String columnName, DataSourceType dataSourceType, CaseType caseType) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(tableName, "Param tableName can't be empty");
        Assert.notNull(columnName, "Param columnName can't be empty");

        if (caseType == null) {
            caseType = CaseType.UNCHANGE;
        }

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        CoreDatabase database = connection.getDatabase();
        CoreTable table = database.getTables().stream().filter(tbl -> tbl.getName().equals(tableName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_TABLE_NOT_FOUND));
        CoreColumn column = table.getColumns().stream().filter(clmn -> clmn.getName().equals(columnName)).findFirst().orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_COLUMN_NOT_FOUND));
        return dataSourceType == null ? column.getRemoveSql(caseType) : CoreBuilder.buildColumn(dataSourceType, column).getRemoveSql(caseType);
    }

    public DataSourceView clone(String id, String name) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(name, "Param name can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        // check name exists
        Optional<DataSource> dataSourceOptOfName = dataSourceDao.findByName(name);
        if (dataSourceOptOfName.isPresent() && !dataSourceOptOfName.get().getId().equals(id)) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.DATA_SOURCE_CLONE_NAME_USED);
        }

        DataSourceParam param = new DataSourceParam();
        param.setId(UUID.randomUUID().toString());
        param.setName(name);
        param.setType(dataSource.getType());
        param.setHost(dataSource.getHost());
        param.setPort(dataSource.getPort());
        param.setUser(dataSource.getUser());
        param.setPass(dataSource.getPass());
        param.setDb(dataSource.getDb());
        param.setOwnerId(dataSource.getOwnerId());
        return this.create(param);
    }

    public DataSourceMoreView more(String id) {
        Assert.notNull(id, "Param id can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));
        DataSourceView dataSourceView = this.fill(dataSourceEntityToView.apply(dataSource));

        DataSourceMoreView dataSourceMoreView = new DataSourceMoreView();
        dataSourceMoreView.setBasic(dataSourceView);
        dataSourceMoreView.setTables(new ArrayList<>());

        CoreConnection connection = CoreBuilder.buildConnection(dataSource);
        CoreDatabase database = connection.getDatabase();
        for (CoreTable table : database.getTables()) {
            TableView tableView = this.convert(table);
            tableView.setColumns(new ArrayList<>());
            for (CoreColumn column : table.getColumns()) {
                ColumnView columnView = this.convert(column);
                tableView.getColumns().add(columnView);
            }
            dataSourceMoreView.getTables().add(tableView);
        }
        return dataSourceMoreView;
    }

    public String password(String id) {
        Assert.notNull(id, "Param id can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));
        return dataSource.getPass();
    }

    public PageResult<DataSourceView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<DataSource> page;
        if (StringUtils.isEmpty(keyword)) {
            page = dataSourceDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = dataSourceDao.findByNameLikeOrHostLikeOrDbLike('%' + keyword + '%', '%' + keyword + '%', '%' + keyword + '%', new SpringPageParamAdapter(pageParam));
        }
        PageResult<DataSource> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataSourceEntityToView).convert(this::fill);
    }

    public void setSuper(String id, UserAndPassParam param) {
        Assert.notNull(id, "Param id can't be empty");
        Assert.notNull(param, "Param param can't be empty");
        Assert.notNull(param.getUser(), "Param param.user can't be empty");
        Assert.notNull(param.getPass(), "Param param.pass can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        CoreConnection connection = CoreBuilder.buildConnection(dataSource.getType(), dataSource.getHost(), dataSource.getPort(),
                param.getUser(), param.getPass(), dataSource.getDb());
        try {
            connection.ping();

            dataSource.setSuperUser(param.getUser());
            dataSource.setSuperPass(param.getPass());
            dataSourceDao.save(dataSource);
        } catch (Exception e) {
            throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
        }
    }

    private String getUserNameByUserId(String userId) {
        UserView userView = userFeign.get(userId);
        return userView == null ? Constant.USER_DELETED : userView.getUserName();
    }

    public List<DataSourceTimeLineView> timelines(String id) {
        Assert.notNull(id, "Param id can't be empty");

        DataSource dataSource = dataSourceDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND,  Constant.DATA_SOURCE_NOT_FOUND));

        Map<String, List<DataSourceTimeLineItemView>> timelineMap = new TreeMap<>();
        SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
        // create
        DataSourceTimeLineItemView createItem = new DataSourceTimeLineItemView();
        createItem.setType(DataSourceTimeLineType.CREATE);
        createItem.setTime(timeFormat.format(dataSource.getCreateDate()));
        createItem.setUser(this.getUserNameByUserId(dataSource.getCreateUserId()));
        createItem.setContent(dataSource.getUrl());
        createItem.setResult(true);
        createItem.setData1(dataSource.getType().name());
        createItem.setData2(dataSource.getType().getValue());

        timelineMap.putIfAbsent(dataFormat.format(dataSource.getCreateDate()), new ArrayList<>());
        timelineMap.get(dataFormat.format(dataSource.getCreateDate())).add(createItem);

        // backup
        List<DataBackupHistory> backupHistories = dataBackupHistoryDao.findByDataSourceId(dataSource.getId());
        for (DataBackupHistory backupHistory : backupHistories) {
            DataSourceTimeLineItemView backupItem = new DataSourceTimeLineItemView();
            backupItem.setType(DataSourceTimeLineType.BACKUP);
            backupItem.setTime(timeFormat.format(backupHistory.getStartDate()));
            backupItem.setUser(this.getUserNameByUserId(backupHistory.getOwnerId()));
            backupItem.setContent(backupHistory.getName());
            backupItem.setResult(DataBackupHistoryStatus.SUCCESS.equals(backupHistory.getStatus()));
            backupItem.setData1(backupHistory.getId());
            backupItem.setData2(backupHistory.getType().name());
            backupItem.setData3(backupHistory.getType().getValue());
            backupItem.setData4(backupHistory.getErrorMessage());


            timelineMap.putIfAbsent(dataFormat.format(backupHistory.getStartDate()), new ArrayList<>());
            timelineMap.get(dataFormat.format(backupHistory.getStartDate())).add(backupItem);
        }

        // recovery
        for (DataBackupHistory backupHistory : backupHistories) {
            List<DataRecoveryHistory> recoveryHistories = dataRecoveryHistoryDao.findByDataBackupHistoryId(backupHistory.getId());
            for (DataRecoveryHistory recoveryHistory : recoveryHistories) {
                DataSourceTimeLineItemView recoveryItem = new DataSourceTimeLineItemView();
                recoveryItem.setType(DataSourceTimeLineType.RECOVERY);
                recoveryItem.setTime(timeFormat.format(recoveryHistory.getStartDate()));
                recoveryItem.setUser(this.getUserNameByUserId(recoveryHistory.getOwnerId()));
                recoveryItem.setContent(recoveryHistory.getName());
                recoveryItem.setResult(DataRecoveryHistoryStatus.SUCCESS.equals(recoveryHistory.getStatus()));
                recoveryItem.setData1(backupHistory.getId());
                recoveryItem.setData2(recoveryHistory.getType().name());
                recoveryItem.setData3(recoveryHistory.getType().getValue());
                recoveryItem.setData4(recoveryHistory.getErrorMessage());

                timelineMap.putIfAbsent(dataFormat.format(recoveryHistory.getStartDate()), new ArrayList<>());
                timelineMap.get(dataFormat.format(recoveryHistory.getStartDate())).add(recoveryItem);
            }
        }

        // upgrade
        List<DataUpgradeDetail> upgradeDetails = dataUpgradeDetailDao.findByDataSourceId(dataSource.getId());
        for (DataUpgradeDetail upgradeDetail : upgradeDetails) {
            DataUpgrade dataUpgrade = dataUpgradeDao.findById(upgradeDetail.getDataUpgradeId()).orElseThrow(() ->
                    new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_UPGRADE_NOT_FOUND));

            DataSourceTimeLineItemView upgradeItem = new DataSourceTimeLineItemView();
            if (DataUpgradeType.INIT.equals(dataUpgrade.getType())) {
                upgradeItem.setType(DataSourceTimeLineType.INIT);
                upgradeItem.setContent("数据项目初始化");
            } else if (DataUpgradeType.UPGRADE.equals(dataUpgrade.getType())) {
                upgradeItem.setType(DataSourceTimeLineType.UPGRADE);
                upgradeItem.setContent(dataUpgrade.getName());
            }
            upgradeItem.setTime(timeFormat.format(upgradeDetail.getStartDate()));
            upgradeItem.setUser(this.getUserNameByUserId(dataUpgrade.getOwnerId()));
            upgradeItem.setResult(DataUpgradeStatus.SUCCESS.equals(upgradeDetail.getStatus()));
            upgradeItem.setData1(upgradeDetail.getScript());
            upgradeItem.setData2(upgradeDetail.getErrorMessage());

            timelineMap.putIfAbsent(dataFormat.format(upgradeDetail.getStartDate()), new ArrayList<>());
            timelineMap.get(dataFormat.format(upgradeDetail.getStartDate())).add(upgradeItem);
        }

        // sort
        List<DataSourceTimeLineView> timelines = new ArrayList<>();
        for (Map.Entry<String, List<DataSourceTimeLineItemView>> entry : timelineMap.entrySet()) {
            entry.getValue().sort(Comparator.comparing(DataSourceTimeLineItemView::getTime));

            DataSourceTimeLineView timeline = new DataSourceTimeLineView();
            timeline.setDate(entry.getKey());
            timeline.setItems(entry.getValue());
            timelines.add(timeline);
        }

        // num
        int num = 1;
        for (DataSourceTimeLineView timeLine : timelines) {
            for (DataSourceTimeLineItemView timeItem : timeLine.getItems()) {
                timeItem.setNum(num);
                num++;
            }
        }

        // relation
        List<DataSourceTimeLineItemView> allTimeItems = new ArrayList<>();
        timelines.stream().map(DataSourceTimeLineView::getItems).forEach(allTimeItems::addAll);
        List<DataSourceTimeLineItemView> backupTimeItems = allTimeItems.stream()
                .filter(item -> DataSourceTimeLineType.BACKUP.equals(item.getType())).collect(Collectors.toList());
        List<DataSourceTimeLineItemView> recoveryTimeItems = allTimeItems.stream()
                .filter(item -> DataSourceTimeLineType.RECOVERY.equals(item.getType())).collect(Collectors.toList());
        for (DataSourceTimeLineItemView recoveryTimeItem : recoveryTimeItems) {
            for (DataSourceTimeLineItemView backupTimeItem : backupTimeItems) {
                if (recoveryTimeItem.getData1().equalsIgnoreCase(backupTimeItem.getData1())) {
                    recoveryTimeItem.setRelateNum1(backupTimeItem.getNum());
                }
            }
        }

        return timelines;
    }

    private ColumnView convert(CoreColumn column) {
        ColumnView view = new ColumnView();
        view.setName(column.getName());
        view.setType(column.getColumnType());
        view.setNullable(column.isNullable());
        view.setIncrement(column.isIncrement());
        view.setComment(column.getComment());
        return view;
    }

    private TableView convert(CoreTable table) {
        TableView view = new TableView();
        view.setName(table.getName());
        view.setComment(table.getComment());
        view.setPrimaryKey(table.getPrimaryKey());
        return view;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        if (Boolean.TRUE.equals(rescheduleWhenStart)) {
            List<DataSource> dataSources = dataSourceDao.findAll();
            for (DataSource dataSource : dataSources) {
                try {
                    scheduler.deleteJob(this.getJobKey(dataSource.getId()));
                    scheduler.scheduleJob(this.getJobDetail(dataSource.getId()), this.getJobTrigger(dataSource.getId()));
                } catch (SchedulerException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }
}
